/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.log4j.chainsaw;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.table.AbstractTableModel;
import org.apache.log4j.Priority;
import org.apache.log4j.Logger;

/**
 * Represents a list of <code>EventDetails</code> objects that are sorted on
 * logging time. Methods are provided to filter the events that are visible.
 * 
 * @author <a href="mailto:oliver@puppycrawl.com">Oliver Burn</a>
 */
class MyTableModel extends AbstractTableModel {

	/** used to log messages **/
	private static final Logger LOG = Logger.getLogger(MyTableModel.class);

	/** use the compare logging events **/
	private static final Comparator MY_COMP = new Comparator() {
		/** @see Comparator **/
		public int compare(Object aObj1, Object aObj2) {
			if ((aObj1 == null) && (aObj2 == null)) {
				return 0; // treat as equal
			} else if (aObj1 == null) {
				return -1; // null less than everything
			} else if (aObj2 == null) {
				return 1; // think about it. :->
			}

			// will assume only have LoggingEvent
			final EventDetails le1 = (EventDetails) aObj1;
			final EventDetails le2 = (EventDetails) aObj2;

			if (le1.getTimeStamp() < le2.getTimeStamp()) {
				return 1;
			}
			// assume not two events are logged at exactly the same time
			return -1;
		}
	};

	/**
	 * Helper that actually processes incoming events.
	 * 
	 * @author <a href="mailto:oliver@puppycrawl.com">Oliver Burn</a>
	 */
	private class Processor implements Runnable {
		/** loops getting the events **/
		public void run() {
			while (true) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					// ignore
				}

				synchronized (mLock) {
					if (mPaused) {
						continue;
					}

					boolean toHead = true; // were events added to head
					boolean needUpdate = false;
					final Iterator it = mPendingEvents.iterator();
					while (it.hasNext()) {
						final EventDetails event = (EventDetails) it.next();
						mAllEvents.add(event);
						toHead = toHead && (event == mAllEvents.first());
						needUpdate = needUpdate || matchFilter(event);
					}
					mPendingEvents.clear();

					if (needUpdate) {
						updateFilteredEvents(toHead);
					}
				}
			}

		}
	}

	/** names of the columns in the table **/
	private static final String[] COL_NAMES = {"Time", "Priority", "Trace",
			"Category", "NDC", "Message"};

	/** definition of an empty list **/
	private static final EventDetails[] EMPTY_LIST = new EventDetails[]{};

	/** used to format dates **/
	private static final DateFormat DATE_FORMATTER = DateFormat
			.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM);

	/** the lock to control access **/
	private final Object mLock = new Object();
	/** set of all logged events - not filtered **/
	private final SortedSet mAllEvents = new TreeSet(MY_COMP);
	/** events that are visible after filtering **/
	private EventDetails[] mFilteredEvents = EMPTY_LIST;
	/** list of events that are buffered for processing **/
	private final List mPendingEvents = new ArrayList();
	/** indicates whether event collection is paused to the UI **/
	private boolean mPaused = false;

	/** filter for the thread **/
	private String mThreadFilter = "";
	/** filter for the message **/
	private String mMessageFilter = "";
	/** filter for the NDC **/
	private String mNDCFilter = "";
	/** filter for the category **/
	private String mCategoryFilter = "";
	/** filter for the priority **/
	private Priority mPriorityFilter = Priority.DEBUG;

	/**
	 * Creates a new <code>MyTableModel</code> instance.
	 * 
	 */
	MyTableModel() {
		final Thread t = new Thread(new Processor());
		t.setDaemon(true);
		t.start();
	}

	////////////////////////////////////////////////////////////////////////////
	// Table Methods
	////////////////////////////////////////////////////////////////////////////

	/** @see javax.swing.table.TableModel **/
	public int getRowCount() {
		synchronized (mLock) {
			return mFilteredEvents.length;
		}
	}

	/** @see javax.swing.table.TableModel **/
	public int getColumnCount() {
		// does not need to be synchronized
		return COL_NAMES.length;
	}

	/** @see javax.swing.table.TableModel **/
	public String getColumnName(int aCol) {
		// does not need to be synchronized
		return COL_NAMES[aCol];
	}

	/** @see javax.swing.table.TableModel **/
	public Class getColumnClass(int aCol) {
		// does not need to be synchronized
		return (aCol == 2) ? Boolean.class : Object.class;
	}

	/** @see javax.swing.table.TableModel **/
	public Object getValueAt(int aRow, int aCol) {
		synchronized (mLock) {
			final EventDetails event = mFilteredEvents[aRow];

			if (aCol == 0) {
				return DATE_FORMATTER.format(new Date(event.getTimeStamp()));
			} else if (aCol == 1) {
				return event.getPriority();
			} else if (aCol == 2) {
				return (event.getThrowableStrRep() == null)
						? Boolean.FALSE
						: Boolean.TRUE;
			} else if (aCol == 3) {
				return event.getCategoryName();
			} else if (aCol == 4) {
				return event.getNDC();
			}
			return event.getMessage();
		}
	}

	////////////////////////////////////////////////////////////////////////////
	// Public Methods
	////////////////////////////////////////////////////////////////////////////

	/**
	 * Sets the priority to filter events on. Only events of equal or higher
	 * property are now displayed.
	 * 
	 * @param aPriority
	 *            the priority to filter on
	 */
	public void setPriorityFilter(Priority aPriority) {
		synchronized (mLock) {
			mPriorityFilter = aPriority;
			updateFilteredEvents(false);
		}
	}

	/**
	 * Set the filter for the thread field.
	 * 
	 * @param aStr
	 *            the string to match
	 */
	public void setThreadFilter(String aStr) {
		synchronized (mLock) {
			mThreadFilter = aStr.trim();
			updateFilteredEvents(false);
		}
	}

	/**
	 * Set the filter for the message field.
	 * 
	 * @param aStr
	 *            the string to match
	 */
	public void setMessageFilter(String aStr) {
		synchronized (mLock) {
			mMessageFilter = aStr.trim();
			updateFilteredEvents(false);
		}
	}

	/**
	 * Set the filter for the NDC field.
	 * 
	 * @param aStr
	 *            the string to match
	 */
	public void setNDCFilter(String aStr) {
		synchronized (mLock) {
			mNDCFilter = aStr.trim();
			updateFilteredEvents(false);
		}
	}

	/**
	 * Set the filter for the category field.
	 * 
	 * @param aStr
	 *            the string to match
	 */
	public void setCategoryFilter(String aStr) {
		synchronized (mLock) {
			mCategoryFilter = aStr.trim();
			updateFilteredEvents(false);
		}
	}

	/**
	 * Add an event to the list.
	 * 
	 * @param aEvent
	 *            a <code>EventDetails</code> value
	 */
	public void addEvent(EventDetails aEvent) {
		synchronized (mLock) {
			mPendingEvents.add(aEvent);
		}
	}

	/**
	 * Clear the list of all events.
	 */
	public void clear() {
		synchronized (mLock) {
			mAllEvents.clear();
			mFilteredEvents = new EventDetails[0];
			mPendingEvents.clear();
			fireTableDataChanged();
		}
	}

	/** Toggle whether collecting events **/
	public void toggle() {
		synchronized (mLock) {
			mPaused = !mPaused;
		}
	}

	/** @return whether currently paused collecting events **/
	public boolean isPaused() {
		synchronized (mLock) {
			return mPaused;
		}
	}

	/**
	 * Get the throwable information at a specified row in the filtered events.
	 * 
	 * @param aRow
	 *            the row index of the event
	 * @return the throwable information
	 */
	public EventDetails getEventDetails(int aRow) {
		synchronized (mLock) {
			return mFilteredEvents[aRow];
		}
	}

	////////////////////////////////////////////////////////////////////////////
	// Private methods
	////////////////////////////////////////////////////////////////////////////

	/**
	 * Update the filtered events data structure.
	 * 
	 * @param aInsertedToFront
	 *            indicates whether events were added to front of the events. If
	 *            true, then the current first event must still exist in the
	 *            list after the filter is applied.
	 */
	private void updateFilteredEvents(boolean aInsertedToFront) {
		final long start = System.currentTimeMillis();
		final List filtered = new ArrayList();
		final int size = mAllEvents.size();
		final Iterator it = mAllEvents.iterator();

		while (it.hasNext()) {
			final EventDetails event = (EventDetails) it.next();
			if (matchFilter(event)) {
				filtered.add(event);
			}
		}

		final EventDetails lastFirst = (mFilteredEvents.length == 0)
				? null
				: mFilteredEvents[0];
		mFilteredEvents = (EventDetails[]) filtered.toArray(EMPTY_LIST);

		if (aInsertedToFront && (lastFirst != null)) {
			final int index = filtered.indexOf(lastFirst);
			if (index < 1) {
				LOG.warn("In strange state");
				fireTableDataChanged();
			} else {
				fireTableRowsInserted(0, index - 1);
			}
		} else {
			fireTableDataChanged();
		}

		final long end = System.currentTimeMillis();
		LOG.debug("Total time [ms]: " + (end - start) + " in update, size: "
				+ size);
	}

	/**
	 * Returns whether an event matches the filters.
	 * 
	 * @param aEvent
	 *            the event to check for a match
	 * @return whether the event matches
	 */
	private boolean matchFilter(EventDetails aEvent) {
		if (aEvent.getPriority().isGreaterOrEqual(mPriorityFilter)
				&& (aEvent.getThreadName().indexOf(mThreadFilter) >= 0)
				&& (aEvent.getCategoryName().indexOf(mCategoryFilter) >= 0)
				&& ((mNDCFilter.length() == 0) || ((aEvent.getNDC() != null) && (aEvent
						.getNDC().indexOf(mNDCFilter) >= 0)))) {
			final String rm = aEvent.getMessage();
			if (rm == null) {
				// only match if we have not filtering in place
				return (mMessageFilter.length() == 0);
			} else {
				return (rm.indexOf(mMessageFilter) >= 0);
			}
		}

		return false; // by default not match
	}
}
